Skip to content

Fix crash when a call signature's type parameter cannot be reused - #64017

Open
Nicolaev Eduard (nikeedw) wants to merge 2 commits into
microsoft:mainfrom
nikeedw:fix-63865-nil-type-parameter
Open

Fix crash when a call signature's type parameter cannot be reused#64017
Nicolaev Eduard (nikeedw) wants to merge 2 commits into
microsoft:mainfrom
nikeedw:fix-63865-nil-type-parameter

Conversation

@nikeedw

Copy link
Copy Markdown

Carries over microsoft/typescript-go#4846, which was approved by Wesley Wigham (@weswigham) and then closed by the repo migration. Re-verified from scratch on this repo's main; the review feedback from that PR (the getDeclaredTypeOfTypeParameter(node.Symbol()) lookup, the test-venue discussion) is already incorporated.

Fixes #63865.

The crash

The PseudoTypeKindSingleCallSignature branch of pseudoTypeToNode appends the result of reuseNode into a call signature's type parameter list without checking it:

for _, tp := range d.TypeParameters {
    res = append(res, b.reuseNode(tp.AsNode()))
}
typeParams = b.f.NewNodeList(res)

reuseNode returns nil whenever the recovery boundary in tryReuseExistingNodeHelper fails — e.g. the constraint references a name that is inaccessible from the file being emitted. The nil is stored in the NodeList and survives to the printer, which dereferences it in NodeList.HasTrailingComma while deciding whether to write a trailing comma. Every other caller of reuseNode copes with the nil; this call site did not.

Why it only reproduces on the incremental path

For a plain build, files in this shape always carry declaration diagnostics (TS2527/TS4023), and emitDeclarationFile skips printing a file that has them — the malformed list is built but never printed. The one production path that prints despite declaration diagnostics is the incremental d.ts shape-signature computation (EmitOnlyForcedDts, sole caller in affectedfileshandler.go). A cold run hashes file text and never prints; the warm run computes the real signature, prints the malformed tree, and panics. That is also why the regression test is a tsctests scenario rather than a compiler test: verified in the original PR, the same fixture under cases/compiler is green both with and without the fix.

The original report came from a mobx-state-tree codebase where all of this falls out of ordinary usage (a generic setter in .actions(self => ({ ... })), MST's unique symbol brands, types.compose pulling models across files).

The fix

Fall back to serializing the type parameter from the checker, mirroring what reuseTypeNode already does for type nodes. typeParameterToDeclaration always returns a node, so the list can no longer contain a nil.

Tests

Two TestTscDeclarationEmit scenarios:

  • dts signature update with type parameters that cannot be reused — a mixed list: the first type parameter (Tag extends string) reuses fine, the second (K extends keyof typeof state) requires the fallback, so the fallback is exercised past the first list slot and reused/serialized nodes coexist in one list. Without the fix it panics in NodeList.HasTrailingComma; with it the baseline shows the signature computed correctly and both declaration diagnostics still reported:

    setField: <Tag extends string, K extends "count" | "name" | unique symbol>(key: K, value: ({
        name: string;
        count: number;
        [brand]: boolean;
    })[K], tag?: Tag) => void;
  • dts signature update with a method type parameter that cannot be reused — the object-literal method counterpart. The method branch (PseudoObjectElementKindMethod) contains the same unchecked reuseNode append, but I could not construct an input where its reuse actually fails — the method context rewrites the same constraint successfully. This scenario pins that behavior (green both before and after the fix), so the sibling site is deliberately left untouched rather than given an untestable fallback.

Verification

  • Both scenarios re-proven red/green against this repo's main (the arrow scenario panics without the fix; the method scenario stays green).
  • go test ./internal/... — 61/61 packages pass; no baseline drift outside the two new files.
  • gofmt, go vet, and the tools/customlint analyzers (including shadow) — clean on the touched files.
  • Mutation spot-check: restricting the fallback to the first list slot is caught by the mixed-fixture scenario.

AI assistance disclosure

Per CONTRIBUTING.md: this patch was authored with the help of Claude Code. #63865 is my own bug report against my own codebase, I drove the investigation, I have read and understand the change, and I will be handling review feedback.

The PseudoTypeKindSingleCallSignature branch of pseudoTypeToNode appended the
result of reuseNode into the type parameter list without checking it. reuseNode
returns nil whenever the recovery boundary in tryReuseExistingNodeHelper fails,
and that nil survived into the NodeList, where the printer dereferenced it in
NodeList.HasTrailingComma while deciding whether to write a trailing comma.

Serialize the type parameter from the checker instead, mirroring the fallback
reuseTypeNode already performs for type nodes.

Two regression scenarios: a mixed list where the first type parameter reuses
fine and the second requires the fallback, and the object-literal method
counterpart, which rewrites the same constraint successfully and pins that
the sibling branch stays panic-free.

Carried over from microsoft/typescript-go#4846 (approved there; repo migrated).

Fixes microsoft#63865

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Panic: nil pointer in NodeList.HasTrailingComma during incremental rebuild (build-mode declaration printer) — 7.0.2 and current nightly

1 participant